package org.springframework.security.config.annotation.method.configuration;

import com.boot.security.service.authorization.TokenAuthorizationManager;
import lombok.extern.slf4j.Slf4j;
import org.aopalliance.intercept.MethodInterceptor;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.SmartInitializingSingleton;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.*;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDecisionVoter;
import org.springframework.security.access.AfterInvocationProvider;
import org.springframework.security.access.PermissionEvaluator;
import org.springframework.security.access.annotation.Jsr250MethodSecurityMetadataSource;
import org.springframework.security.access.annotation.Jsr250Voter;
import org.springframework.security.access.annotation.SecuredAnnotationSecurityMetadataSource;
import org.springframework.security.access.expression.method.*;
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
import org.springframework.security.access.intercept.AfterInvocationManager;
import org.springframework.security.access.intercept.AfterInvocationProviderManager;
import org.springframework.security.access.intercept.RunAsManager;
import org.springframework.security.access.intercept.aopalliance.MethodSecurityInterceptor;
import org.springframework.security.access.intercept.aspectj.AspectJMethodSecurityInterceptor;
import org.springframework.security.access.method.DelegatingMethodSecurityMetadataSource;
import org.springframework.security.access.method.MethodSecurityMetadataSource;
import org.springframework.security.access.prepost.PostInvocationAdviceProvider;
import org.springframework.security.access.prepost.PreInvocationAuthorizationAdvice;
import org.springframework.security.access.prepost.PreInvocationAuthorizationAdviceVoter;
import org.springframework.security.access.prepost.PrePostAnnotationSecurityMetadataSource;
import org.springframework.security.access.vote.AuthenticatedVoter;
import org.springframework.security.access.vote.RoleVoter;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationTrustResolver;
import org.springframework.security.authentication.DefaultAuthenticationEventPublisher;
import org.springframework.security.config.annotation.ObjectPostProcessor;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.core.GrantedAuthorityDefaults;
import org.springframework.util.Assert;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
 * @author 霜寒 <1621856595@qq.com>
 * @description 魔改 {@link GlobalMethodSecurityConfiguration}，
 * 修改了 {@link #accessDecisionManager()} 方法，
 * 覆盖了 {@link MethodSecurityInterceptor 类}
 * @date 2021/1/19 15:50
 **/

@Slf4j
@EnableGlobalMethodSecurity(prePostEnabled = true, proxyTargetClass = true)
@Configuration(proxyBeanMethods = false)
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public class GlobalMethodSecurityConfiguration implements ImportAware, SmartInitializingSingleton, BeanFactoryAware {

    private ObjectPostProcessor<Object> objectPostProcessor = new ObjectPostProcessor<Object>() {
        @Override
        public <T> T postProcess(T object) {
            throw new IllegalStateException(ObjectPostProcessor.class.getName()
                    + " is a required bean. Ensure you have used @" + EnableGlobalMethodSecurity.class.getName());
        }

    };

    private DefaultMethodSecurityExpressionHandler defaultMethodExpressionHandler = new DefaultMethodSecurityExpressionHandler();

    private AuthenticationManager authenticationManager;

    private boolean disableAuthenticationRegistry;

    private AnnotationAttributes enableMethodSecurity;

    private BeanFactory context;

    private MethodSecurityExpressionHandler expressionHandler;

    private MethodSecurityInterceptor methodSecurityInterceptor;

    @Bean
    public MethodInterceptor methodSecurityInterceptor(MethodSecurityMetadataSource methodSecurityMetadataSource) {
        this.methodSecurityInterceptor = isAspectJ() ? new AspectJMethodSecurityInterceptor()
                : new MethodSecurityInterceptor();
        this.methodSecurityInterceptor.setAccessDecisionManager(accessDecisionManager());
        this.methodSecurityInterceptor.setAfterInvocationManager(afterInvocationManager());
        this.methodSecurityInterceptor.setSecurityMetadataSource(methodSecurityMetadataSource);
        RunAsManager runAsManager = runAsManager();
        if (runAsManager != null) {
            this.methodSecurityInterceptor.setRunAsManager(runAsManager);
        }
        return this.methodSecurityInterceptor;
    }

    @Override
    public void afterSingletonsInstantiated() {
        try {
            initializeMethodSecurityInterceptor();
        } catch (Exception ex) {
            throw new RuntimeException(ex);
        }
        PermissionEvaluator permissionEvaluator = getSingleBeanOrNull(PermissionEvaluator.class);
        if (permissionEvaluator != null) {
            this.defaultMethodExpressionHandler.setPermissionEvaluator(permissionEvaluator);
        }
        RoleHierarchy roleHierarchy = getSingleBeanOrNull(RoleHierarchy.class);
        if (roleHierarchy != null) {
            this.defaultMethodExpressionHandler.setRoleHierarchy(roleHierarchy);
        }
        AuthenticationTrustResolver trustResolver = getSingleBeanOrNull(AuthenticationTrustResolver.class);
        if (trustResolver != null) {
            this.defaultMethodExpressionHandler.setTrustResolver(trustResolver);
        }
        GrantedAuthorityDefaults grantedAuthorityDefaults = getSingleBeanOrNull(GrantedAuthorityDefaults.class);
        if (grantedAuthorityDefaults != null) {
            this.defaultMethodExpressionHandler.setDefaultRolePrefix(grantedAuthorityDefaults.getRolePrefix());
        }
    }

    private <T> T getSingleBeanOrNull(Class<T> type) {
        try {
            return this.context.getBean(type);
        } catch (NoSuchBeanDefinitionException ignored) {
            return null;
        }
    }

    private void initializeMethodSecurityInterceptor() throws Exception {
        if (this.methodSecurityInterceptor == null) {
            return;
        }
        this.methodSecurityInterceptor.setAuthenticationManager(authenticationManager());
    }

    protected AfterInvocationManager afterInvocationManager() {
        if (prePostEnabled()) {
            AfterInvocationProviderManager invocationProviderManager = new AfterInvocationProviderManager();
            ExpressionBasedPostInvocationAdvice postAdvice = new ExpressionBasedPostInvocationAdvice(
                    getExpressionHandler());
            PostInvocationAdviceProvider postInvocationAdviceProvider = new PostInvocationAdviceProvider(postAdvice);
            List<AfterInvocationProvider> afterInvocationProviders = new ArrayList<>();
            afterInvocationProviders.add(postInvocationAdviceProvider);
            invocationProviderManager.setProviders(afterInvocationProviders);
            return invocationProviderManager;
        }
        return null;
    }

    protected RunAsManager runAsManager() {
        return null;
    }

    protected AccessDecisionManager accessDecisionManager() {
        List<AccessDecisionVoter<?>> decisionVoters = new ArrayList<>();
        if (prePostEnabled()) {
            ExpressionBasedPreInvocationAdvice expressionAdvice = new ExpressionBasedPreInvocationAdvice();
            expressionAdvice.setExpressionHandler(getExpressionHandler());
            decisionVoters.add(new PreInvocationAuthorizationAdviceVoter(expressionAdvice));
        }
        if (jsr250Enabled()) {
            decisionVoters.add(new Jsr250Voter());
        }
        RoleVoter roleVoter = new RoleVoter();
        GrantedAuthorityDefaults grantedAuthorityDefaults = getSingleBeanOrNull(GrantedAuthorityDefaults.class);
        if (grantedAuthorityDefaults != null) {
            roleVoter.setRolePrefix(grantedAuthorityDefaults.getRolePrefix());
        }
        decisionVoters.add(roleVoter);
        decisionVoters.add(new AuthenticatedVoter());
        return new TokenAuthorizationManager(decisionVoters);
    }

    protected MethodSecurityExpressionHandler createExpressionHandler() {
        return this.defaultMethodExpressionHandler;
    }

    protected final MethodSecurityExpressionHandler getExpressionHandler() {
        if (this.expressionHandler == null) {
            this.expressionHandler = createExpressionHandler();
        }
        return this.expressionHandler;
    }

    protected MethodSecurityMetadataSource customMethodSecurityMetadataSource() {
        return null;
    }

    protected AuthenticationManager authenticationManager() throws Exception {
        if (null == this.authenticationManager) {
            DefaultAuthenticationEventPublisher eventPublisher = this.objectPostProcessor
                    .postProcess(new DefaultAuthenticationEventPublisher());
            AuthenticationManagerBuilder auth = new AuthenticationManagerBuilder(this.objectPostProcessor);
            auth.authenticationEventPublisher(eventPublisher);
            configure(auth);
            this.authenticationManager = (this.disableAuthenticationRegistry)
                    ? getAuthenticationConfiguration().getAuthenticationManager() : auth.build();
        }
        return this.authenticationManager;
    }

    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        this.disableAuthenticationRegistry = true;
    }

    @Bean
    public MethodSecurityMetadataSource methodSecurityMetadataSource() {
        List<MethodSecurityMetadataSource> sources = new ArrayList<>();
        ExpressionBasedAnnotationAttributeFactory attributeFactory = new ExpressionBasedAnnotationAttributeFactory(
                getExpressionHandler());
        MethodSecurityMetadataSource customMethodSecurityMetadataSource = customMethodSecurityMetadataSource();
        if (customMethodSecurityMetadataSource != null) {
            sources.add(customMethodSecurityMetadataSource);
        }
        boolean hasCustom = customMethodSecurityMetadataSource != null;
        boolean isPrePostEnabled = prePostEnabled();
        boolean isSecuredEnabled = securedEnabled();
        boolean isJsr250Enabled = jsr250Enabled();
        Assert.state(isPrePostEnabled || isSecuredEnabled || isJsr250Enabled || hasCustom,
                "In the composition of all global method configuration, "
                        + "no annotation support was actually activated");
        if (isPrePostEnabled) {
            sources.add(new PrePostAnnotationSecurityMetadataSource(attributeFactory));
        }
        if (isSecuredEnabled) {
            sources.add(new SecuredAnnotationSecurityMetadataSource());
        }
        if (isJsr250Enabled) {
            GrantedAuthorityDefaults grantedAuthorityDefaults = getSingleBeanOrNull(GrantedAuthorityDefaults.class);
            Jsr250MethodSecurityMetadataSource jsr250MethodSecurityMetadataSource = this.context
                    .getBean(Jsr250MethodSecurityMetadataSource.class);
            if (grantedAuthorityDefaults != null) {
                jsr250MethodSecurityMetadataSource.setDefaultRolePrefix(grantedAuthorityDefaults.getRolePrefix());
            }
            sources.add(jsr250MethodSecurityMetadataSource);
        }
        return new DelegatingMethodSecurityMetadataSource(sources);
    }

    @Bean
    public PreInvocationAuthorizationAdvice preInvocationAuthorizationAdvice() {
        ExpressionBasedPreInvocationAdvice preInvocationAdvice = new ExpressionBasedPreInvocationAdvice();
        preInvocationAdvice.setExpressionHandler(getExpressionHandler());
        return preInvocationAdvice;
    }

    @Override
    public final void setImportMetadata(AnnotationMetadata importMetadata) {
        Map<String, Object> annotationAttributes = importMetadata
                .getAnnotationAttributes(EnableGlobalMethodSecurity.class.getName());
        this.enableMethodSecurity = AnnotationAttributes.fromMap(annotationAttributes);
    }

    @Autowired(required = false)
    public void setObjectPostProcessor(ObjectPostProcessor<Object> objectPostProcessor) {
        this.objectPostProcessor = objectPostProcessor;
        this.defaultMethodExpressionHandler = objectPostProcessor.postProcess(this.defaultMethodExpressionHandler);
    }

    @Autowired(required = false)
    public void setMethodSecurityExpressionHandler(List<MethodSecurityExpressionHandler> handlers) {
        if (handlers.size() != 1) {
            log.debug("Not autowiring MethodSecurityExpressionHandler since size != 1. Got " + handlers);
            return;
        }
        this.expressionHandler = handlers.get(0);
    }

    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        this.context = beanFactory;
    }

    private AuthenticationConfiguration getAuthenticationConfiguration() {
        return this.context.getBean(AuthenticationConfiguration.class);
    }

    private boolean prePostEnabled() {
        return enableMethodSecurity().getBoolean("prePostEnabled");
    }

    private boolean securedEnabled() {
        return enableMethodSecurity().getBoolean("securedEnabled");
    }

    private boolean jsr250Enabled() {
        return enableMethodSecurity().getBoolean("jsr250Enabled");
    }

    private boolean isAspectJ() {
        return enableMethodSecurity().getEnum("mode") == AdviceMode.ASPECTJ;
    }

    private AnnotationAttributes enableMethodSecurity() {
        if (this.enableMethodSecurity == null) {
            // if it is null look at this instance (i.e. a subclass was used)
            EnableGlobalMethodSecurity methodSecurityAnnotation = AnnotationUtils.findAnnotation(getClass(),
                    EnableGlobalMethodSecurity.class);
            Assert.notNull(methodSecurityAnnotation, () -> EnableGlobalMethodSecurity.class.getName() + " is required");
            Map<String, Object> methodSecurityAttrs = AnnotationUtils.getAnnotationAttributes(methodSecurityAnnotation);
            this.enableMethodSecurity = AnnotationAttributes.fromMap(methodSecurityAttrs);
        }
        return this.enableMethodSecurity;
    }
}
